package com.dyz.wsw.domain.admin.service.impl;

import com.dyz.wsw.common.consts.web.HttpConst;
import com.dyz.wsw.common.consts.web.JwtConst;
import com.dyz.wsw.common.enumerator.ServiceCode;
import com.dyz.wsw.common.ex.ServiceException;
import com.dyz.wsw.common.pojo.auth.Principal;
import com.dyz.wsw.common.pojo.cache.UserCachePO;
import com.dyz.wsw.common.utils.converter.Json;
import com.dyz.wsw.common.utils.security.MD5;
import com.dyz.wsw.common.web.JsonResult;
import com.dyz.wsw.common.utils.security.AES;
import com.dyz.wsw.common.utils.security.JWTs;
import com.dyz.wsw.common.utils.security.RSA;
import com.dyz.wsw.config.WswProperties;
import com.dyz.wsw.domain.admin.pojo.*;
import com.dyz.wsw.domain.admin.repository.cache.IUserCacheRepository;
import com.dyz.wsw.domain.admin.repository.persist.AdminUserRepository;
import com.dyz.wsw.domain.admin.repository.persist.ILoginLogRepository;
import com.dyz.wsw.domain.admin.repository.persist.UserInfoRepository;
import com.dyz.wsw.domain.admin.repository.persist.UserRepository;
import com.dyz.wsw.domain.admin.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author dyz
 * Time 2024/6/26 23:59
 */
@Service
@Slf4j
public class UserServiceImpl implements IUserService, JwtConst, HttpConst {

    //--------------------------------------------------
    private final AdminUserRepository adminUserRepository;


    //--------------------------------------------------

    private final PasswordEncoder passwordEncoder;

    private final UserRepository userRepository;

    private final ILoginLogRepository loginLogRepository;

    private static final Map<String,UserLoginInfoVO> tempCache = new HashMap<>();

    private final IUserCacheRepository userCacheRepository;

    private final WswProperties wswProperties;

    @Autowired
    private UserInfoRepository userInfoRepository;

    public UserServiceImpl(
            PasswordEncoder passwordEncoder,
            UserRepository userRepository,
            ILoginLogRepository loginLogRepository,
            IUserCacheRepository userCacheRepository,
            WswProperties wswProperties,
            AdminUserRepository adminUserRepository
    ){
        log.info("创建登录服务对象:LoginServiceImpl");
        this.passwordEncoder = passwordEncoder;
        this.userRepository = userRepository;
        this.loginLogRepository = loginLogRepository;
        this.userCacheRepository = userCacheRepository;
        this.wswProperties = wswProperties;
        this.adminUserRepository = adminUserRepository;
    }

    /**
     * 登陆处理逻辑
     * @param loginInfoDTO 用户传入的值
     * @param ip 客户端ip
     * @param userAgent 客户端设备信息
     * @return 登录处理结果
     */
    @Override
    public Mono<JsonResult> handleLogin(LoginInfoDTO loginInfoDTO, String ip, String userAgent) {
        log.debug("开始处理【根据表单信息处理登录业务】，表单信息：{}", loginInfoDTO);
        String username = loginInfoDTO.getUsername();
        String password = loginInfoDTO.getPassword();
        String key = loginInfoDTO.getKey();


        return validateLogin(username,password,key)
                .flatMap(adminUser -> {
                    log.debug("登录成功!!!{}", adminUser);

                    return Mono.just(JsonResult.ok(adminUser));
                })
                .defaultIfEmpty(JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED,"用户名不存在"));

//                .flatMap(this::parsePassword)//2.尝试还原密码
//                .flatMap(login -> userRepository.findUserLoginInfoByUsername(login.getUsername())//3.根据用户名查找用户信息
//                        .flatMap(userLoginInfoVO -> loginCheck(login.getPassword(),userLoginInfoVO))//4.登录认证
//                        .flatMap(userLoginInfoVO -> recordLoginLog(userLoginInfoVO,ip,userAgent))//5.记录本次登录日志信息
//                        .flatMap(this::issueToken)//6.颁发令牌
//                        .flatMap(jwt->doLoginResult(jwt,tempCache.get(login.getUsername())))
//                .defaultIfEmpty(JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED,"用户名不存在")));
    }



    /**
     * 登录校验
     * @param username 用户名
     * @param password 密码
     * @param key 密钥
     * @return 用户对象或者空流
     */
    private Mono<AdminUser> validateLogin(String username, String password,String key) {
        //TODO 这边还得优化应该一次性把权限也查出来
        return adminUserRepository.findUserByUsername(username)
                .handle((adminUser,sink) -> {
                    String rawPassword = getRawPassword(key,password);
                    log.debug("rawPassword: {}", rawPassword);
                    boolean matches = passwordEncoder.matches(rawPassword, adminUser.getPassword());
                    if(!matches){
                        log.warn("user: {},密码错误",adminUser.getUsername());
                        sink.error(new ServiceException(ServiceCode.ERROR_UNAUTHORIZED,"用户名或密码错误!"));
                        return;
                    }
                    if(adminUser.getEnable()==0){
                        log.warn("user: {}被封禁,请联系管理员!",adminUser.getUsername());
                        sink.error(new ServiceException(ServiceCode.ERROR_UNAUTHORIZED_DISABLED,"此用户被封禁!"));
                        return;
                    }
                    sink.next(adminUser);
                    sink.complete();
                });
    }


    /**
     * 处理登出逻辑
     * @param userId 用户请求id
     * @return 登出处理结果
     */
    @Override
    public Mono<JsonResult> handleLogout(Long userId) {
        //1.删除redis中的数据
        return userCacheRepository.deleteUserInfo(userId)
                .then(Mono.just(JsonResult.ok("登出成功")));
    }

    /**
     * 查询用户信息
     * @param principal 当事人
     * @return 用户信息
     */
    @Override
    public Mono<JsonResult> getUserInfo(Principal principal) {
        log.debug("开始处理【根据当事人查询用户信息业务】，当事人：{}", principal);

        return userInfoRepository.findByUserId(principal.getId())
                .map(JsonResult::ok);
    }


    /**
     * 向缓存中存储用户状态信息，同时将jwt等信息返回给客户端
     * @param jwt jwt令牌
     * @param loginInfo 用户登录信息
     * @return 返回登录处理结果
     */
    private Mono<JsonResult> doLoginResult(String jwt,UserLoginInfoVO loginInfo){

        List<String> permissions = loginInfo.getPermissions();

        String authoritiesJsonString = Json.toJson(permissions);

        UserCachePO userCachePO = new UserCachePO();
        userCachePO.setEnable(ENABLE_STATE_ON);
        userCachePO.setAuthoritiesJsonString(authoritiesJsonString);
        log.debug("向缓存中存入用户状态数据：{}",userCachePO);
        //TODO 需要优化一下，在spring启动时尝试连接redis，如果连接失败，则spring整个应用就不起来了
        //TODO 或者选择这里思考其他方案，至少让客户端不会连接卡顿
        Mono<Void> doCache = userCacheRepository.saveUserState(loginInfo.getId(), userCachePO);

        UserLoginResultVO userLoginResultVO = new UserLoginResultVO()
                .setId(loginInfo.getId())
                .setUsername(loginInfo.getUsername())
                .setAvatar(loginInfo.getAvatar())
                .setToken(jwt)
                .setAuthorities(permissions);
        log.debug("即将返回用户的登录结果：{}", userLoginResultVO);

        //删除临时缓存中的数据
        tempCache.remove(loginInfo.getUsername());

        return doCache.then(Mono.just(JsonResult.ok(userLoginResultVO)));
    }


    /**
     * 记录本次登录信息，1.更新user表部分信息 2.插入一条登录日志信息记录
     * 这里我是考虑有可能出错，不知道前条流异常或者失败等情况，后条流怎么处理法，如果登录日志相关异常，重点排查此方法
     * @param userLoginInfoVO 用户登录信息
     * @param ip ip地址
     * @param userAgent 客户端信息
     * @return 登录日志信息
     */
    private Mono<LoginLogDO> recordLoginLog(UserLoginInfoVO userLoginInfoVO, String ip, String userAgent){
        // 折衷处理，暂时将部分数据放在map中供后续流临时使用
        tempCache.put(userLoginInfoVO.getUsername(),userLoginInfoVO);

        log.debug("开始处理【记录本次登录信息业务】，本次用户信息：{}，ip：{}，客户端信息：{}",userLoginInfoVO,ip,userAgent);
        Long id = userLoginInfoVO.getId();
        String username = userLoginInfoVO.getUsername();
        Integer loginCount = userLoginInfoVO.getLoginCount();
        LocalDateTime now = LocalDateTime.now();
        Mono<Void> updateLogin = userRepository.updateLastLogin(id, loginCount + 1, ip, now);
        LoginLogDO loginLog = new LoginLogDO();
        loginLog.setUserId(id);
        loginLog.setUsername(username);
        loginLog.setIp(ip);
        loginLog.setUserAgent(userAgent);
        loginLog.setGmtLogin(now);
        loginLog.setGmtCreate(now);
        loginLog.setGmtModified(now);
        Mono<LoginLogDO> save = loginLogRepository.save(loginLog);
        return updateLogin.then(save);
    }



    /**
     * 登录校验
     * @param rawPassword 原始密码
     * @param userLoginInfoVO 校验对象
     * @return 校验结果，正常返回userLoginInfoVO
     */
    private Mono<UserLoginInfoVO> loginCheck(String rawPassword,UserLoginInfoVO userLoginInfoVO){
        log.debug("开始处理【登录校验业务】，校验对象信息：{}",userLoginInfoVO);
        return Mono.just(userLoginInfoVO)
                .handle((loginInfo,sink) -> {
                    //1.校验密码
                    boolean matches = passwordEncoder.matches(rawPassword, userLoginInfoVO.getPassword());
                    if(!matches){
                        String message = "用户名或密码错误！";
                        log.warn(message);
                        sink.error(new ServiceException(ServiceCode.ERROR_UNAUTHORIZED,message));
                        return;
                    }
                    //2.查看账号是否被封禁
                    if(userLoginInfoVO.getEnable()==0){
                        String message = "此用户被封禁！";
                        sink.error(new ServiceException(ServiceCode.ERROR_UNAUTHORIZED_DISABLED,message));
                        return;
                    }
                    //3.登录若无异常将userLoginInfoVO传至下游
                    sink.next(userLoginInfoVO);
                });
    }


    /**
     * 颁发令牌
     * @return jwt令牌
     */
    private Mono<String> issueToken(LoginLogDO loginLog){
        log.debug("开始处理【jwt令牌颁发业务】，用户登录日志信息：{}",loginLog);
        //颁发步骤
        Map<String,Object> payload = new HashMap<>();
        payload.put(CLAIM_JWT_ISS,"dyz");
        payload.put(CLAIM_USER_ID,loginLog.getUserId());
        payload.put(CLAIM_USER_NAME,loginLog.getUsername());
        // 将ip和 userAgent作为特征存储jwt，校验时也是先算出ip+userAgent的md5，来检验是否存在盗用问题
        payload.put(CLAIM_JWT_JWI, MD5.encrypt(loginLog.getIp()));
        //也可以单独为这个jwt准备一份钥匙,这里就用密码加解密的私钥
        //如果单独准备了,还注意一起将校验时用到的私钥引用也修改了
        String jwt = JWTs.generate(payload, wswProperties.getPrivateKey());
        log.debug("本次请求生成的jwt：{}",jwt);
        return Mono.just(jwt);
    }


    /**
     * 尝试还原密码
     * @param key 加密后的AES密钥
     * @param encryptedPassword 加密密码
     * @return 原始密码
     */
    private String getRawPassword(String key,String encryptedPassword){
        String rawPassword;
        try {
            String aesKey = RSA.decrypt(key, wswProperties.getPrivateKey());
            rawPassword = AES.decrypt(encryptedPassword, aesKey);
        } catch (Exception e) {
            throw new ServiceException(ServiceCode.ERROR_ILLEGAL_PASSWORD,"Illegal password");
        }
        return rawPassword;
    }

    /**
     * 解析密码,用于将加密密码还原成明文密码
     * @param loginInfoDTO 解析对象
     * @return 一个只包含用户名和原始密码的UserDto流
     */
    private Mono<LoginInfoDTO> parsePassword(LoginInfoDTO loginInfoDTO){
        log.debug("开始处理【根据表单信息解析出原始密码】，表单信息：{}", loginInfoDTO);
          return Mono.just(loginInfoDTO)
                    .handle((login, sink) -> {
                        //密码解密步骤
                        //1.获取通过公钥加密后的AES密钥
                        String key = login.getKey();
                        String password = login.getPassword();

                        //2.通过私钥将AES密钥还原
                        String aesKey;
                        String rawPassword;
                        try {
                            aesKey = RSA.decrypt(key,wswProperties.getPrivateKey());
                            //3.用AES密钥将密码还原成明文
                            rawPassword = AES.decrypt(password,aesKey);
                        } catch (Exception e) {
                            String message = "密码解析错误";
                            log.warn(message);
                            //未按指定的公钥加密AES密钥或者AES密钥与AES密文不匹配
                            sink.error(new ServiceException(ServiceCode.ERROR_UNAUTHORIZED,message));
                            return;
                        }
                        login.setPassword(rawPassword);
                        sink.next(login);
                    });
    }
}
